/*
 * The Yard Utilties - http://www.theyard.net/
 * 
 * Copyright (c) 2008 by Vlideshow, Inc..  All Rights Resrved.
 * 
 * This library is free software; you can redistribute it and/or modify it under the 
 * terms of the GNU Lesser General Public License as published by the Free Software 
 * Foundation; either version 2.1 of the License, or (at your option) any later 
 * version. 
 * 
 * This library is distributed in the hope that it will be useful, but WITHOUT ANY 
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 
 * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along 
 * with this library; if not, write to the Free Software Foundation, Inc., 
 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 
 */
package net.theyard.components.sm
{
  import net.theyard.components.sm.events.StateChangeEvent;
  import net.theyard.components.Utils;

  import flash.events.EventDispatcher;
  import flash.events.IEventDispatcher;
  import flash.events.TimerEvent;
  import flash.utils.Timer;

  /**
   * This is an abstract 'helper' class designed to allow you to more easily set up a StateMachine class.
   *
   * See the NetConnectionStateMachine for an example concrete implementation.
   *
   * @see net.theyard.components.netconnectionsm.NetConnectionStateMachine
   */
  public class StateMachine extends EventDispatcher
  {
    private var states:Object;
    private var _state:State;
    private var numStateTransitions:uint;

    static public const INVALID_STATE:String = 
      "YardComponent.StateMachine.State.INVALID";

    /**
     * Initialize a state machine.
     *
     * @param target The target IEventDispather
     */
    public function StateMachine(target:IEventDispatcher=null)
    {
      super(target);
      states = new Object();
      addState(new InvalidState());
      numStateTransitions = 0;
    }

    /**
     * Adds a state to the set of supported states.
     *
     * You should call this method to add all states before you set
     * the first state and certainly beore folks start using the state.
     */
    protected function addState(state: State): void
    {
      if (states == null) 
        throw new Error("Must call super() in constructor");

      // be sure the state name is valid

      if (
          state == null
          || state.toString() == null
          || state.toString().length == 0)
      {
        throw new ArgumentError("Must pass valid state name");
      }

      // verify that the state name is unique

      if (states[state.toString()] != null)
        throw new Error("State \"" + state.toString() + "\" is already defined.");

      // add state

      states[state.toString()] = state;

      // and by default, set the starting state to this state.

      _state = state;
    }

    /**
     * Set the starting state.
     *
     * Sets the starting state but does not dispatch an event.
     *
     * @param stateName The starting state
     */
    protected function setStartingState(stateName: String): void
    {
      var newState:State = states[stateName];

      if (newState == null) 
      {
        throw new ArgumentError("No state setup for state name: " + stateName);
      }
      _state = newState;
    }

    /**
     * Get the current state as a State object.
     *
     * @return The current state.
     * @see #State
     */
    protected function get state():State
    {
      return _state;
    }

    /**
     * Defer the given function and object until this state machine has completed a transition.
     *
     * <p>
     * This function defers calling a given function until all state
     * transitions have dispatched their events; it then calls the
     * function.
     * </p>
     * <p>
     * This is useful to avoid re-entrant conditions.  For example some
     * event handlers on a NetConnectionStateMachine might immediately
     * call disconnect() when they see a successful connection made by
     * calling the disconnect in the middle of their event handler for
     * a StateChange event. 
     * </p>
     * <p>
     * Unfortunately the state change event handler is fired in the
     * middle of a state transition, but the disconnect() event causes
     * another callback to be called immediately on the state machine,
     * even though it hasn't finished transitioning yet.  This can
     * make things tricky.
     * </p>
     * <p>
     * So this method marshalls up all the arguments to the function called
     * and then sets a timer to call the function as soon as we unwind
     * completely from firing all event handlers for the current state
     * transition.
     * </p>
     * <p>
     * The correct use case is to use this method to marshall
     * any calls on public interfaces for your State Machine so that anyone
     * who attempts to call a potential state-changing external interface on
     * your class is guaranteed to not <b>actually</b> call it in the middle
     * of processing a state transition.
     * </p>
     */
    protected function deferCallUntilStableState(
        func:Function, obj:Object, ...args):void
    {
      if (numStateTransitions == 0) 
      {
        // we're currently stable; run now
        try 
        {
          func.apply(obj, args);
        }
        catch (ex:Error) 
        {
          Utils.ytrace(
              "Exception: " + ex.name 
              + ": " + ex.errorID + ": " + ex.message);
          throw ex;
        }
      } 
      else
      {
        // we're transitioning states; set a timer and execute it then
        // once out of this stack of call functions.

        var t:Timer = new Timer(0, 1);
        Utils.ytrace("Deferring function for later execution");

        t.addEventListener(
            TimerEvent.TIMER_COMPLETE,
            function (event:TimerEvent):void
            {
            Utils.ytrace("Executing deferred function");
            func.apply(obj, args);
            });

        t.start();
      }
    }

    /**
     * Transition to a new state, sending state transition events as needed.
     *
     * @param stateName New state to go to.
     */
    public function gotoState(stateName:String):void
    {
      var f:Utils;
      numStateTransitions = numStateTransitions + 1;
      var oldState:String = (_state != null ? _state.toString():"");
      Utils.ytrace("["+oldState+"]=>["+stateName+"]");
      _state=states[stateName];

      // Our state changed.  We should let folks know.
      this.dispatchEvent(this.getNewStateChangeEvent(stateName, oldState));
      numStateTransitions = numStateTransitions - 1;

    }

    /**
     * Get the current state.
     *
     * @return The current state.
     */
    public function getState():String
    {
      return _state.toString();
    }

    /**
     * Override this if you want to provide your own events
     * (derived from StateChangeEvent) instead of just changing
     * using unique names for states.
     *
     * @param newState New state the StateMachine should transition to.
     * @param oldState Old state transitioning from
     * @param args Other args.
     * 
     * @return A new event derived from StateChangeEvent
     */
    protected function getNewStateChangeEvent(
        newState:String,
        oldState:String,
        ...args):StateChangeEvent
    {
      return new StateChangeEvent(newState, oldState);	
    }
  }
}
